#ifndef SIMULATOR_HPP
#define SIMULATOR_HPP
#include "FileReader.hpp"
#include "ClWrapper.hpp"
#include "DeviceWrapper.hpp"
#include "Types.hpp"
#include <cmath>
#include <string>
#include <cstring>
#define PI 3.141559265359
class Simulator
{
public:
    Simulator(const std::string& filename);
    ~Simulator()
    {
        delete [] cells;
        delete [] particle_ids;
        delete [] masses;
        delete [] densities;
        delete [] pressures;
        delete [] positions;
        delete [] velocities;
    }
    inline FileReader& getReader(){return reader;}
    inline cl_real lengthx(){return x_max-x_min;}
    inline cl_real lengthy(){return y_max-y_min;}
    inline cl_real lengthz(){return z_max-z_min;}
    inline const cl_real& xmin(){return x_min;}
    inline const cl_real& ymin(){return y_min;}
    inline const cl_real& zmin(){return z_min;}
    inline const cl_real4& velocity(const int& pID){return velocities[pID];}
    inline const cl_real& density(const int& pID){return densities[pID];}
    inline const cl_real& mass(const int& pID){return masses[pID];}
    inline const cl_real& pressure(const int& pID){return pressures[pID];}
    inline const cl_real4& position(const int& pID){return positions[pID];}
    inline const cl_uint& NumParticles(){return NParticles;}
    inline cl_real dt(){return delta_t;}
    inline cl_real endTime(){return end_t;}
    void printContainers(); //TO_DO
    inline void QueryPlatformDevice(const std::string& kernelsFilename);
    void MemoryBuffersAllocation();

    //Kernels
    inline cl::Kernel emptyCellsKernel();
    inline cl::Kernel fillCellsKernel();
    //i=1 -> euler integrator
    //i=2 -> yusukeIntegrator
    inline cl::Kernel velPosIntegratorKernel(const int& i);
    inline cl::Kernel accelerationKernel();
    inline cl::Kernel densityCalculationKernel();
    inline cl::Kernel boundariesKernel();

    inline void setKernels(const int& i);
    inline void finishQueue();
    void runWithGraphics(const cl_uint &repeat);
    void runWithVTK();

private:
    FileReader reader;
    //Parameters
    cl_real delta_t,end_t;
    int vtkFreq,WORK_GROUP_SIZE,partfreq;
    cl_uint nx,ny,nz;
    cl_real x_min,x_max,y_min,y_max,z_min,z_max;
    cl_real len_x,len_y,len_z;
    cl_real cut_r;
    real viscosity;
    real k_gas;
    real rho0;
    cl_uint GLOBAL_CELLS_SIZE;
    cl_uint GLOBAL_PARTICLES_SIZE;
    std::string part_out_name,vtk_out_name;

    cl_real4 gravity;
    int reflectx;
    int reflecty;
    int reflectz;

    //Simulation containers
    cl_uint NParticles,NCells;
    cl_int* cells;
    cl_int* particle_ids;
    cl_real* masses;
    cl_real* densities;
    cl_real* pressures;
    cl_real4* positions;
    cl_real4* velocities;
    //Do we need inverseMasses?

    //OpenCl stuff
    cl::Device device;
    cl::Context context;
    cl::CommandQueue queue;
    cl::Program::Sources sources;
    cl::Program program;

    //Buffers
    cl::Buffer posBuffer;
    cl::Buffer velBuffer;
    cl::Buffer cellsBuffer;
    cl::Buffer part_id_Buffer;
    cl::Buffer densityBuffer;
    cl::Buffer invDenBuffer;
    cl::Buffer massesBuffer;
    cl::Buffer pressureBuffer;
    cl::Buffer accelBuffer;

    //Kernels
    cl::Kernel densityK;
    cl::Kernel accelerationK;
    cl::Kernel updatingPVK;
    cl::Kernel boundariesK;
    cl::Kernel emptyCellsK;
    cl::Kernel fillCellsK;
};

inline void Simulator::QueryPlatformDevice(const std::string& kernelsFilename)
{
    // Getting information about the Platforms
    ClWrapper platofrmCL;
    std::cout << "Available Platforms" << std::endl;
    platofrmCL.printPlatforms(std::cout);

    // Get the Platform from the USER
    cl::Platform pl = platofrmCL.getPlatform();
    std::cout << "Using the Following Platform: " << std::endl;
    ClWrapper platform_sel(pl);
    platform_sel.printPlatforms(std::cout);

    // Obtain information about the Devices present in the Selected Platform
    std::cout << "Available Devices" << std::endl;
    DeviceWrapper deviceCL(pl);
    deviceCL.printDevices(std::cout);

    //Set the Device, Context, Program, Commandqueue, sources. Can be modified later to make number of Programs instead of one program.
    deviceCL.getDeviceProgram(kernelsFilename, device, context, queue, sources, program);
    std::cout << "Using the Following Device: " << std::endl;
    DeviceWrapper deviceSel(device);
    deviceSel.printDevices(std::cout);
}

inline cl::Kernel Simulator::emptyCellsKernel()
{
    cl::Kernel kSetCellsToMinus = cl::Kernel(program, "setCellsToMinus");
    kSetCellsToMinus.setArg(0,cellsBuffer);
    kSetCellsToMinus.setArg(1,NCells);
    return kSetCellsToMinus;
}

inline cl::Kernel Simulator::fillCellsKernel()
{
    cl::Kernel kAssembleCells = cl::Kernel(program, "assembleCells");
    kAssembleCells.setArg(0,posBuffer);
    kAssembleCells.setArg(1,part_id_Buffer);
    kAssembleCells.setArg(2,cellsBuffer);
    kAssembleCells.setArg(3,NParticles);
    kAssembleCells.setArg(4,nx);
    kAssembleCells.setArg(5,ny);
    kAssembleCells.setArg(6,nz);
    kAssembleCells.setArg(7,len_x);
    kAssembleCells.setArg(8,len_y);
    kAssembleCells.setArg(9,len_z);
    return kAssembleCells;
}

inline cl::Kernel Simulator::velPosIntegratorKernel(const int& i)
{
    std::string kernelName = (i==1)?"eulerIntegrator":"yusukeIntegrator";
    cl::Kernel kIntegrator = cl::Kernel(program,kernelName.c_str());
    kIntegrator.setArg(0,posBuffer);
    kIntegrator.setArg(1,velBuffer);
    kIntegrator.setArg(2,accelBuffer);
    kIntegrator.setArg(3,delta_t);
    kIntegrator.setArg(4,NParticles);
    return kIntegrator;
}

inline cl::Kernel Simulator::accelerationKernel()
{
    real Cpress=22.5/(PI*pow(cut_r,6));
    real Cvisco=45.0/(PI*pow(cut_r,6))*viscosity;
    std::cout<<"press const: "<<Cpress<<"\n";
    std::cout<<"visco const: "<<Cvisco<<"\n";
    cl::Kernel kAccele = cl::Kernel(program,"accelerationCalculation");
    kAccele.setArg(0,posBuffer);
    kAccele.setArg(1,accelBuffer);
    kAccele.setArg(2,cl::__local(sizeof(cl_real4) * WORK_GROUP_SIZE));
    kAccele.setArg(3,cl::__local(sizeof(cl_real4) * WORK_GROUP_SIZE));
    kAccele.setArg(4,cellsBuffer);
    kAccele.setArg(5,part_id_Buffer);
    kAccele.setArg(6,velBuffer);
    kAccele.setArg(7,pressureBuffer);
    kAccele.setArg(8,invDenBuffer);
    kAccele.setArg(9,massesBuffer);
    kAccele.setArg(10,gravity);
    kAccele.setArg(11,NParticles);
    kAccele.setArg(12,nx);
    kAccele.setArg(13,ny);
    kAccele.setArg(14,nz);
    kAccele.setArg(15,len_x);
    kAccele.setArg(16,len_y);
    kAccele.setArg(17,len_z);
    kAccele.setArg(18,reflectx);
    kAccele.setArg(19,reflecty);
    kAccele.setArg(20,reflectz);
    kAccele.setArg(21,x_min);
    kAccele.setArg(22,y_min);
    kAccele.setArg(23,z_min);
    kAccele.setArg(24,x_max);
    kAccele.setArg(25,y_max);
    kAccele.setArg(26,z_max);
    kAccele.setArg(27,Cpress);
    kAccele.setArg(28,Cvisco);
    kAccele.setArg(29,cut_r);

    return kAccele;
}

inline cl::Kernel Simulator::densityCalculationKernel()
{
    real Cdens = 315.0/(64.0*pow(cut_r,9)*PI);
    std::cout<<"Density const: "<<Cdens<<"\n";
    cl::Kernel kDensityC = cl::Kernel(program,"densityPressureCalculation");
    kDensityC.setArg(0,posBuffer);
    kDensityC.setArg(1,densityBuffer);
    kDensityC.setArg(2,invDenBuffer);
    kDensityC.setArg(3,pressureBuffer);
    kDensityC.setArg(4,cl::__local(sizeof(cl_real4) * WORK_GROUP_SIZE));
    kDensityC.setArg(5,cellsBuffer);
    kDensityC.setArg(6,part_id_Buffer);
    kDensityC.setArg(7,massesBuffer);
    kDensityC.setArg(8,NParticles);
    kDensityC.setArg(9,nx);
    kDensityC.setArg(10,ny);
    kDensityC.setArg(11,nz);
    kDensityC.setArg(12,len_x);
    kDensityC.setArg(13,len_y);
    kDensityC.setArg(14,len_z);
    kDensityC.setArg(15,reflectx);
    kDensityC.setArg(16,reflecty);
    kDensityC.setArg(17,reflectz);
    kDensityC.setArg(18,x_min);
    kDensityC.setArg(19,y_min);
    kDensityC.setArg(20,z_min);
    kDensityC.setArg(21,x_max);
    kDensityC.setArg(22,y_max);
    kDensityC.setArg(23,z_max);
    kDensityC.setArg(24,Cdens);
    kDensityC.setArg(25,k_gas);
    kDensityC.setArg(26,rho0);
    kDensityC.setArg(27,cut_r*cut_r);

    return kDensityC;
}

inline cl::Kernel Simulator::boundariesKernel()
{
    cl::Kernel kSetPeriodicity = cl::Kernel(program, "setBoundaries");
    kSetPeriodicity.setArg(0,posBuffer);
    kSetPeriodicity.setArg(1,NParticles);
    kSetPeriodicity.setArg(2,x_min);
    kSetPeriodicity.setArg(3,x_max);
    kSetPeriodicity.setArg(4,y_min);
    kSetPeriodicity.setArg(5,y_max);
    kSetPeriodicity.setArg(6,z_min);
    kSetPeriodicity.setArg(7,z_max);
    kSetPeriodicity.setArg(8,reflectx);    
    kSetPeriodicity.setArg(9,reflecty);
    kSetPeriodicity.setArg(10,reflectz);
    kSetPeriodicity.setArg(11,densityBuffer);
    kSetPeriodicity.setArg(12,accelBuffer);

    return kSetPeriodicity;
}

inline void Simulator::setKernels(const int& i)
{
    try
    {
        densityK = densityCalculationKernel();
        accelerationK = accelerationKernel();
        updatingPVK = velPosIntegratorKernel(i);
        boundariesK = boundariesKernel();
        emptyCellsK = emptyCellsKernel();
        fillCellsK = fillCellsKernel();
    }
    catch(cl::Error er)
    {
        printf("ERROR: %s(%d)\n", er.what(), er.err());
    }
}

inline void Simulator::finishQueue()
{
    try
    {
        queue.finish();
    }
    catch(cl::Error er)
    {
        printf("ERROR: %s(%d)\n", er.what(), er.err());
    }
}



#endif // SIMULATOR_HPP
